package org.beynet.jsonbinding.bind;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

public class JSONBClassStructure {
    
    public JSONBClassStructure(JSONBContext context,Class<?> cl) {
        this.currentClass = cl;
        expectedElementName = null ;
        JSONElement annotation = cl.getAnnotation(JSONElement.class);
        if (annotation!=null) {
            if (annotation.name()!=null) {
                expectedElementName = annotation.name();
            }
        }
        // scanning methods
        // ----------------
        for (Method m : cl.getDeclaredMethods()) {
            annotation = m.getAnnotation(JSONElement.class);
            if (m.getName().startsWith("get")) {
                
                String propertyName = m.getName().substring(3);
                
                if (annotation!=null && logger.isDebugEnabled()) {
                    logger.debug("Method "+m.toString()+" from class "+cl.getCanonicalName()+" annotated with "+JSONElement.class.toString());
                }
                int modifiers = m.getModifiers();
                if (annotation!=null) {
                    if (!Modifier.isPublic(modifiers)) {
                        throw new IllegalArgumentException("method annotated with "+JSONElement.class.toString()+" is not public");
                    }
                }
                else {
                    if (!Modifier.isPublic(modifiers)) {
                        continue;
                    }
                }
                Class<?>[] parameters = m.getParameterTypes();
                if (parameters.length!=0) {
                    throw new IllegalArgumentException("method annotated with "+JSONElement.class.toString()+" must not contain arguments");
                }
                
                
                
                Class<?> returnType = m.getReturnType();
                Method setter = null;
                try {
                    setter = cl.getMethod("set"+propertyName, returnType);
                } catch (Exception e) {
                }
                if (setter==null && ! List.class.isAssignableFrom(returnType)) {
                    throw new IllegalArgumentException("no setter found associated with annotated method :"+m.getName()+" in class "+cl.getCanonicalName());
                }
                
                // retrieve property name
                // ---------------------
                Map<Method,String> map = new HashMap<>();
                final String name ;
                if (annotation!=null && annotation.name()!=null) {
                    name = annotation.name();
                }
                else {
                    name = propertyName;
                }
                map.put(m, name);
                // storing method by name
                methods.add(map);
                if (setter!=null) setterByElementName.put(name, setter);
                getterByElementName.put(name, m);
                
                final JSONBClassStructure child ;
                // if the result of this method will be map to an array                
                if (List.class.isAssignableFrom(returnType)) {
                    Type genericReturnType = m.getGenericReturnType();

                    if(genericReturnType instanceof ParameterizedType) {
                        ParameterizedType type = (ParameterizedType) genericReturnType;
                        Type[] typeArguments = type.getActualTypeArguments();
                        returnType = (Class<?>) typeArguments[0];
                    }
                    else {
                        throw new IllegalArgumentException("methods returning a list but we are unable to cmpute generic type : methodName = "+m.getName()+" class ="+cl.getName());
                    }
                }
                if (!String.class.equals(returnType) && !BigDecimal.class.equals(returnType) &&
                        !Boolean.class.equals(returnType)) {
                    if (context.isClassesInScope(returnType)==false) {
                        child = new JSONBClassStructure(context, returnType);
                        context.addClassIfNotAlreadyInScope(returnType, child);
                    }
                    else {
                        child = context.getClassInScopeStructure(returnType);
                    }
                    childsPerName.put(name,child);
                }   
            }
        }
    }
    
    
    /**
     * @return all the fields that could generate a json value
     */
    protected Iterator<Map<Field,String>> getFields() {
        final Iterator<Map<Field,String>> iterator = fields.iterator();
        return new Iterator<Map<Field,String>>() {
            @Override
            public boolean hasNext() {
                return iterator.hasNext();
            }

            @Override
            public Map<Field,String> next() {
                return iterator.next();
            }

            @Override
            public void remove() {
               throw new UnsupportedOperationException();
            }
            
        };
    }
    
    /**
     * @return  all the methods that could generate a json value
     */
    public Iterator<Map<Method,String>> getMethods() {
        final Iterator<Map<Method,String>> iterator = methods.iterator();
        return new Iterator<Map<Method,String>>() {
            @Override
            public boolean hasNext() {
                return iterator.hasNext();
            }

            @Override
            public Map<Method,String> next() {
                return iterator.next();
            }

            @Override
            public void remove() {
               throw new UnsupportedOperationException();
            }
            
        };
    }
    
    /**
     * @param name
     * @return
     */
    public JSONBClassStructure getChildStructureByChildName(String name) {
        return(childsPerName.get(name));
    }
    
    public Method getSetterByElementName(String name) {
        return setterByElementName.get(name);
    }
    public Method getGetterByElementName(String name) {
        return getterByElementName.get(name);
    }
    
    public String getExpectedElementName() {
        return expectedElementName;
    }
    
    public Class<?> getCurrentClass() {
        return currentClass;
    }
    
    private String expectedElementName ;
    private Class<?> currentClass ;
    private List<Map<Field,String>> fields = new ArrayList<>();
    private Map<String,Field> fieldsByName = new HashMap<>();
    private List<Map<Method,String>> methods = new ArrayList<>();
    private Map<String,Method> setterByElementName = new HashMap<>();
    private Map<String,Method> getterByElementName = new HashMap<>();
    
    
    private Map<String,JSONBClassStructure> childsPerName = new HashMap<>();
    
    private final static Logger logger = Logger.getLogger(JSONBClassStructure.class);
}
